package com.qd.core.dblog.audit;


import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.cdqidi.util.RedisTemplateUtil;
import com.qd.common.sys.enums.OperatorType;
import com.qd.core.aspect.operarorlog.OperatorAopLog;
import com.qd.core.aspect.operarorlog.OperatorLogManager;
import com.qd.system.model.DataLog;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author sjk
 */
@Intercepts({@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class})})
@Slf4j
public class AuditLogInterceptor implements Interceptor {

    private final StringRedisTemplate stringRedisTemplate;
    private static final Map<String, String> TABLES = new ConcurrentHashMap<>(1024);
    private static final Map<String, TableInfo> TABLE_INFO_MAP = new ConcurrentHashMap<>(1024);

    public AuditLogInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        OperatorAopLog operatorAopLog = OperatorLogManager.getOperatorAopLog();
        if (null == operatorAopLog) {
            return invocation.proceed();
        }
        if (!operatorAopLog.isAuditLog()) {
            //不记录数据日志
            return invocation.proceed();
        }
        //方法参数
        Object[] args = invocation.getArgs();
        if (null == args || args.length != 2) {
            return invocation.proceed();
        }
        MappedStatement ms = (MappedStatement) args[0];
        if (null == ms) {
            return invocation.proceed();
        }
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        if (!sqlCommandType.equals(SqlCommandType.INSERT) && !sqlCommandType.equals(SqlCommandType.UPDATE) && !sqlCommandType.equals(SqlCommandType.DELETE)) {
            if (log.isDebugEnabled()) {
                log.debug("不处理的sqlCommandType: {}", sqlCommandType);
            }
            return invocation.proceed();
        }
        Object parameter = args[1];

        BoundSql boundSql = ms.getBoundSql(parameter);
        if (null == boundSql) {
            return invocation.proceed();
        }
        Configuration configuration = ms.getConfiguration();
        MetaObject metaObject = configuration.newMetaObject(parameter);

        DataLog dataLog = new DataLog();
        try {
            TableInfo tableInfo = getTableInfo(boundSql.getSql());
            String tableName = getTableNameBySql(boundSql.getSql());
            if (StringUtils.hasLength(tableName)) {
                dataLog.setTableName(tableName);
            }
            ContentValue contentValue = getValue(metaObject, parameter, tableInfo);
            if (null != contentValue.value) {
                dataLog.setContentObj(contentValue.value);
            }
            dataLog.setDaoMethodName(ms.getId());
            dataLog.setSqlCommandType(ms.getSqlCommandType().name());
            dataLog.setRawSql(boundSql.getSql());

            if (sqlCommandType.equals(SqlCommandType.UPDATE)) {
                boolean isSelectOldValue = !operatorAopLog.getOperatorType().equals(OperatorType.IMPORT);
                if (isSelectOldValue) {
                    isSelectOldValue = !CharSequenceUtil.contains(ms.getId(), "batch");
                    if (isSelectOldValue) {
                        isSelectOldValue = !CharSequenceUtil.contains(ms.getId(), "Batch");
                    }
                }
                if (isSelectOldValue) {
                    String oldValue = getRedisCache(contentValue, tableInfo);
                    if (StringUtils.hasLength(oldValue)) {
                        dataLog.setHistoryContent(oldValue);
                    }
                }
            } else if (sqlCommandType.equals(SqlCommandType.DELETE)) {
                String oldValue = getRedisCache(contentValue, tableInfo);
                if (StringUtils.hasLength(oldValue)) {
                    dataLog.setHistoryContent(oldValue);
                }
            }
            contentValue.freeData();
        } catch (Exception e) {
           log.error("AuditLogInterceptor",e);
            operatorAopLog.clearDataLog();
            return invocation.proceed();
        }
        Object proceed = invocation.proceed();
        try {
            //执行操作以前
            Integer proceedIntVal = (Integer) proceed;
            dataLog.setSqlReturnId(proceedIntVal);
        } catch (Exception ignored) {
        }
        //异步写入数据库
        operatorAopLog.addDataLog(dataLog);

        return proceed;
    }

    @Override
    public Object plugin(Object target) {
        //可以在这个方法中提前进行拦截对象类型判断从而提高性能，如我们仅对Executor拦截，可以这么写：
        //只对要拦截定制的对象生成代理
        if (target instanceof Executor) {
            //调用插件
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }

    private ContentValue getValue(MetaObject metaObject, Object parameter, TableInfo tableInfo) {
        ContentValue contentValue;
        if (metaObject.hasGetter("param1")) {
            contentValue = createContentValue(metaObject.getValue("param1"), tableInfo);
        } else if (metaObject.hasGetter("et")) {
            contentValue = createContentValue(metaObject.getValue("et"), tableInfo);
        } else {
            if (parameter instanceof String) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Byte) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Short) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Integer) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Long) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Float) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Double) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Boolean) {
                contentValue = createContentValue(parameter);
            } else if (parameter instanceof Character) {
                contentValue = createContentValue(parameter);
            } else {
                contentValue = createContentValue(parameter, tableInfo);
            }
        }
        return contentValue;
    }

    private ContentValue createContentValue(Object parameter) {
        ContentValue contentValue = new ContentValue();
        Map<String, Object> map = new HashMap<>(1);
        map.put("key", parameter);
        contentValue.setIdValue(parameter);
        contentValue.setValue(map);
        return contentValue;
    }

    private ContentValue createContentValue(Object param, TableInfo tableInfo) {
        ContentValue contentValue = new ContentValue();
        Object idVal;
        if (null != tableInfo) {
            try {
                idVal = tableInfo.getPropertyValue(param, tableInfo.getKeyProperty());
            } catch (Exception e) {
                idVal = param;
            }
            contentValue.setIdValue(idVal);
        }
        contentValue.setValue(ObjectUtil.clone(param));
        return contentValue;
    }

    @SneakyThrows
    private String getRedisCache(ContentValue contentValue, TableInfo tableInfo) {
        if (null == tableInfo) {
            return null;
        }
        Object idVal = contentValue.getIdValue();
        if (null != idVal) {
            if (idVal instanceof List) {
                //批量的，目前不处理
                return null;
            } else if (idVal instanceof String){
                String idVarStr = (String) idVal;
                String key = RedisTemplateUtil.getRedisCacheKey(tableInfo.getTableName() + "_" + tableInfo.getKeyProperty() + "_{0}", idVal);
                if (idVarStr.length() > 200) {
                    log.error("取缓存异常,tableName: {},redis key: {}", tableInfo.getTableName(), key);
                    return null;
                }
                return stringRedisTemplate.opsForValue().get(key);
            }

        }
        return null;
    }

    private TableInfo getTableInfo(String sql) {
        TableInfo tableInfo = TABLE_INFO_MAP.get(sql);
        if (null == tableInfo) {
            String tableName = getTableNameBySql(sql);
            if (StringUtils.hasLength(tableName)) {
                tableInfo = TableInfoHelper.getTableInfo(tableName);
            }
            if (null != tableInfo) {
                TABLE_INFO_MAP.put(sql, tableInfo);
            }
        }
        return tableInfo;
    }

    @SneakyThrows
    public String getTableNameBySql(String sql) {
        String table = TABLES.get(sql);
        if (null == table) {
            Statement statement = CCJSqlParserUtil.parse(sql);
            if (statement instanceof Insert) {
                table = ((Insert) statement).getTable().getName();
            } else if (statement instanceof Update) {
                table = ((Update) statement).getTable().getName();
            } else if (statement instanceof Delete) {
                table = ((Delete) statement).getTable().getName();
            }
            if (StringUtils.hasLength(table)) {
                TABLES.put(sql, table);
            }
        }
        return table;
    }

    @Data
    @Accessors(chain = true)
    static class ContentValue {
        private Object value;
        private Object idValue;

        public void freeData() {
            this.value = null;
            this.idValue = null;
        }
    }
}
